package pl.com.like.sequoia.biojava.util.impl;

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.GZIPOutputStream;

import org.biojava.bio.seq.db.FetchURL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

import pl.com.like.sequoia.biojava.util.IConnectionHelper;
import pl.com.like.sequoia.biojava.util.IDirHelper;
import pl.com.like.sequoia.dao.bio.IBioObjectAlignDao;
import pl.com.like.sequoia.dao.bio.IBioObjectHandlerDao;
import pl.com.like.sequoia.dao.bio.IBioParamDao;
import pl.com.like.sequoia.exceptions.LogicalException;
import pl.com.like.sequoia.exceptions.TechnicalException;
import pl.com.like.sequoia.model.bio.BioObjectAlign;
import pl.com.like.sequoia.model.bio.BioObjectHandler;
import pl.com.like.sequoia.model.bio.enums.BioObjectType;
import pl.com.like.sequoia.model.bio.enums.FileTypeEnum;
import pl.com.like.sequoia.model.bio.enums.SourceType;

/**
 * class contains helpful utility methods used while connecting to servers
 * 
 * @author Pawel Wolanski
 * 
 */
public class ConnectionHelper extends AbstractHelper implements IConnectionHelper {

	private final static Logger log = LoggerFactory.getLogger(ConnectionHelper.class);

	@Autowired
	private IBioObjectHandlerDao bioObjectHandlerDao;
	@Autowired
	private IBioObjectAlignDao bioObjectAlignDao;
	@Autowired
	private IBioParamDao bioParamDao;
	private IDirHelper dirHelper;
	/**
	 * GZIP header magic number.
	 */
	public final static int GZIP_MAGIC = 0x8b1f;

	/**
	 * {@inheritDoc}
	 * 
	 * @throws LogicalException
	 */
	public File downloadFile(URL url, File destFile) throws TechnicalException, LogicalException {
		Boolean exists = Boolean.FALSE;
		try {
			if (destFile == null) {
				log.warn("destination file was not given. File is written to temporary directory.");
				destFile = File.createTempFile("sequoia", "file.tmp");
			}
			FileOutputStream outPut = new FileOutputStream(destFile);
			GZIPOutputStream gzOutPut = new GZIPOutputStream(outPut);
			PrintWriter pw = new PrintWriter(gzOutPut);
			log.info(MessageFormat.format("fetching file from URL: {0}", url.toExternalForm()));
			InputStream input = FileHelper.getInstance().getInputStream(url);
			log.info(MessageFormat.format("writing file to: {0}", destFile.getAbsolutePath()));
			BufferedReader fileBuffer = new BufferedReader(new InputStreamReader(input));
			String line;
			while ((line = fileBuffer.readLine()) != null) {
				exists = Boolean.TRUE;
				pw.println(line);
			}
			pw.flush();
			pw.close();
			outPut.close();
			input.close();
			if (Boolean.FALSE.equals(exists)) {
				// output file should be deleted
				destFile.delete();
				String msg = MessageFormat.format("bad request. File on URL {0} does not exists", url.toExternalForm());
				throw new LogicalException(msg);
			}
		} catch (IOException e) {
			String errMsg = "cannot create temporary file on server";
			log.error(errMsg, e);
			throw new TechnicalException(errMsg, e);
		}
		return destFile;

	}

	/**
	 * {@inheritDoc}
	 */
	public String generateAddress(String id, FileTypeEnum fileType) throws LogicalException {
		String urlAddr = null;
		if (isPDBFormat(fileType)) {
			// URLs should be modifiable without recompiling source.
			String baseUrl = configService.getPdbSiteFormat();
			urlAddr = MessageFormat.format(baseUrl, id, fileType.getRetType());
		} else {
			String rettype = getRetType(fileType);
			FetchURL seqURL = new FetchURL("Genbank", "text");
			String baseurl = seqURL.getbaseURL();
			String db = seqURL.getDB();
			urlAddr = baseurl + db + "&id=" + id + "&rettype=" + rettype;
		}
		return urlAddr;
	}

	public IBioObjectAlignDao getBioObjectAlignDao() {
		return bioObjectAlignDao;
	}

	public IBioObjectHandlerDao getBioObjectHandlerDao() {
		return bioObjectHandlerDao;
	}

	public IBioParamDao getBioParamDao() {
		return bioParamDao;
	}

	public List<BioObjectHandler> getComparisonPair(String idComparison) {
		return this.getBioObjectAlignDao().getComparisonPair(idComparison);
	}

	public IDirHelper getDirHelper() {
		return dirHelper;
	}

	public BioObjectHandler getObject(String id, FileTypeEnum fileType) throws LogicalException {
		File returnFile = null;
		BioObjectHandler bioHandler = null;
		// checks if exists in database
		bioHandler = bioObjectHandlerDao.get(id, fileType);
		if (null != bioHandler) {
			returnFile = new File(bioHandler.getPath());
			// checks if file still exists in file system
			if (!returnFile.exists()) {
				String message = MessageFormat.format("given file {0} does not exists in path {1}", id, bioHandler
						.getPath());
				log.warn(message);
				bioParamDao.removeAll(bioHandler.getBioParams());
				bioObjectHandlerDao.remove(bioHandler);
				// recursively checks again for bioObjectHandler object
				bioHandler = getObject(id, fileType);
			}
		} else {
			// download from internet
			String urlString = generateAddress(id, fileType);
			URL url;
			try {
				url = new URL(urlString);
				returnFile = downloadFile(url, returnFile);
				bioHandler = registerFile(id, fileType, returnFile, SourceType.INTERNET);
			} catch (MalformedURLException e) {
				e.printStackTrace();
				String message = MessageFormat.format("given URL is incorrect {0}", urlString);
				log.error(message, e);
				throw new LogicalException(message);
			} catch (TechnicalException e) {
				String message = MessageFormat.format("Could not download file {0}, {1}", id, fileType);
				log.error(message, e);
			}
		}
		return bioHandler;
	}

	/**
	 * get symbol assigned for Genbank file type.
	 * 
	 * @param type sequoia's enum identifies type of returned file
	 * @return raw symbol assigned for Genbank file type.
	 */
	public String getRetType(FileTypeEnum type) {
		String retType = FileTypeEnum.defaultValue().getRetType();
		if (null != type) {
			retType = type.getRetType();
		}
		return retType;
	}

	public BioObjectAlign getSearchAlign(String idObject) {
		return bioObjectAlignDao.get(idObject);
	}

	public List<BioObjectAlign> getSearchAlignList(String idObject, FileTypeEnum fileType) {
		return bioObjectAlignDao.getSearchList(idObject);
	}

	/**
	 * {@inheritDoc}
	 */
	public List<BioObjectHandler> getSearchList(String idObject, FileTypeEnum fileType, Integer size,
			Boolean extendedSearch, Integer firstResult) {
		return this.bioObjectHandlerDao.searchObjects(idObject, fileType, extendedSearch, size, firstResult);
	}

	/**
	 * {@inheritDoc}
	 */
	public Boolean isGenbankType(FileTypeEnum type) {
		Boolean result = Boolean.FALSE;
		if (null == type) {
			log.debug("given type is null.");
		} else {
			if (FileTypeEnum.GENBANK.equals(type) || FileTypeEnum.GENPEPT.equals(type)
					|| FileTypeEnum.FASTA.equals(type)) {
				log.debug(MessageFormat.format("given type {0} is supported by Genbank", type));
				result = Boolean.TRUE;
			}
		}
		return result;
	}

	/**
	 * check if input stream is gzipped
	 * 
	 * @param in checked input stream
	 * @return if input stream begins on special GZIP number, returns Boolean.TRUE
	 * @throws LogicalException throws if wasn't possible to read anything from stream
	 * @throws TechnicalException
	 */
	public Boolean isGzipped(InputStream in) throws TechnicalException {
		Boolean result = Boolean.FALSE;
		CRC32 crc = new CRC32();
		CheckedInputStream cin = new CheckedInputStream(in, crc);
		crc.reset();
		// Check header magic
		try {
			if (readUShort(cin) == GZIP_MAGIC) {
				result = Boolean.TRUE;
			}
		} catch (IOException e) {
			throw new TechnicalException(MessageFormat.format("could not read information from input stream {0}", in
					.toString()), e);
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	public Boolean isPDBFormat(FileTypeEnum type) {
		Boolean result = Boolean.FALSE;
		if (type != null) {
			switch (type) {
				case PDB_ZIP:
				case PDB:
				case PDB_HEAD:
					result = Boolean.TRUE;
					break;
				default:
					result = Boolean.FALSE;
			}
		}
		return result;
	}

	/** {@inheritDoc} */
	public List<String> readFile(InputStream in) throws LogicalException {
		List<String> content = new ArrayList<String>();
		if (null != in) {
			BufferedReader reader = new BufferedReader(new InputStreamReader(in));
			String newLine = null;
			try {
				while ((newLine = reader.readLine()) != null) {
					content.add(newLine);
				}
			} catch (IOException e) {
				log.error(MessageFormat.format("Cannot open input stream {0}", in.toString()), e);
				throw new LogicalException(MessageFormat.format("Cannot open input stream {0}", in.toString()), e);
			}
		}
		return content;
	}

	/**
	 * Reads unsigned byte.
	 */
	private int readUByte(InputStream in) throws IOException {
		int b = in.read();
		if (b == -1)
			throw new EOFException();
		return b;
	}

	/**
	 * Reads unsigned short in Intel byte order.
	 */
	private int readUShort(InputStream in) throws IOException {
		int b = readUByte(in);
		return (readUByte(in) << 8) | b;
	}

	public BioObjectAlign registerAlign(String idFile, FileTypeEnum fileType, File returnFile, SourceType type,
			BioObjectType bioType, BioObjectHandler bioA, BioObjectHandler bioB) {
		Assert.notNull(bioA);
		Assert.notNull(bioB);
		returnFile = saveInRepo(idFile, fileType, returnFile);
		BioObjectAlign align = registerAlignInDb(idFile, returnFile, bioA, bioB);
		return align;
	}

	/**
	 * Register result of align computation in database
	 * 
	 * @param idFile
	 * @param returnFile
	 * @param bioA
	 * @param bioB
	 * @return saved in database model object
	 */
	public BioObjectAlign registerAlignInDb(String idFile, File returnFile, BioObjectHandler bioA, BioObjectHandler bioB) {
		BioObjectAlign align = new BioObjectAlign();
		align.setName(idFile);
		align.setPath(returnFile.getAbsolutePath());
		align.setBioObjectA(bioA);
		align.setBioObjectB(bioB);
		bioObjectAlignDao.save(align);
		if (log.isDebugEnabled()) {
			log.debug(MessageFormat.format("BioObjectHandler has been saved {0}", align));
		}
		return align;
	}

	/**
	 * Get temporary file, copy to repository and save info to database.
	 * 
	 * @param idFile
	 * @param fileType
	 * @param returnFile
	 * @param type
	 * @throws LogicalException
	 */
	public BioObjectHandler registerFile(String idFile, FileTypeEnum fileType, File returnFile, SourceType type)
			throws LogicalException {
		returnFile = saveInRepo(idFile, fileType, returnFile);
		return registerObjInDb(idFile, fileType, returnFile, type);
	}

	/**
	 * Register given file in database
	 * 
	 * @param idFile
	 * @param fileType
	 * @param returnFile
	 * @param type
	 * @param bioType depending on this value different objects are created. {@link BioObjectHandler} for standard
	 *            objects and {@link BioObjectAlign} for align results.
	 * @return
	 * 
	 */
	public BioObjectHandler registerObjInDb(String idFile, FileTypeEnum fileType, File returnFile, SourceType type) {
		// fill method fro standard behavior on adding new proteins.
		BioObjectHandler bio = new BioObjectHandler();
		bio.setFileType(fileType);
		bio.setName(idFile);
		bio.setPath(returnFile.getAbsolutePath());
		bio.setSourceType(type);
		bio.setChecked(Boolean.FALSE);
		bioObjectHandlerDao.save(bio);
		if (log.isDebugEnabled()) {
			log.debug(MessageFormat.format("BioObjectHandler has been saved {0}", bio));
		}
		return bio;
	}

	/**
	 * Copy file from temporary directory to repository
	 * 
	 * @param idFile
	 * @param fileType
	 * @param returnFile
	 * @return
	 * @throws LogicalException
	 */
	public File saveInRepo(String idFile, FileTypeEnum fileType, File returnFile) throws LogicalException {
		File finalFile = dirHelper.createDestFile(idFile, fileType);
		log.debug(MessageFormat.format("file {0} has been created", finalFile.getAbsoluteFile()));
		try {
			FileChannel fileIn = new FileInputStream(returnFile).getChannel();
			FileChannel fileOut = new FileOutputStream(finalFile).getChannel();
			fileIn.transferTo(0, fileIn.size(), fileOut);
			log.debug(MessageFormat.format("data has been copied from {0} to {1}", returnFile.getAbsoluteFile(),
					finalFile.getAbsoluteFile()));
		} catch (FileNotFoundException e) {
			String message = MessageFormat.format("could not find one of objects {0} or {1}", returnFile
					.getAbsoluteFile(), finalFile.getAbsoluteFile());
			log.error(message, e);
			throw new LogicalException(message, e);
		} catch (IOException e) {
			String message = MessageFormat.format("could not read, write one of objects {0} or {1}", returnFile
					.getAbsoluteFile(), finalFile.getAbsoluteFile());
			log.error(message, e);
			throw new LogicalException(message, e);
		}
		return finalFile;
	}

	/**
	 * Copy file from temporary directory to repository
	 * 
	 * @param idFile
	 * @param fileType
	 * @param returnFile
	 * @param type
	 * @return
	 * @throws LogicalException
	 * @deprecated use saveInRepo(String idFile, FileTypeEnum fileType, File returnFile) instead
	 */
	@Deprecated
	public File saveInRepo(String idFile, FileTypeEnum fileType, File returnFile, SourceType type)
			throws LogicalException {
		return saveInRepo(idFile, fileType, returnFile);
	}

	public void setBioObjectAlignDao(IBioObjectAlignDao bioObjectAlignDao) {
		this.bioObjectAlignDao = bioObjectAlignDao;
	}

	public void setBioObjectHandlerDao(IBioObjectHandlerDao bioObjectHandlerDao) {
		this.bioObjectHandlerDao = bioObjectHandlerDao;
	}

	public void setBioParamDao(IBioParamDao bioParamDao) {
		this.bioParamDao = bioParamDao;
	}

	public void setDirHelper(IDirHelper dirHelper) {
		this.dirHelper = dirHelper;
	}
}
